跳到主要内容

Java 反射学习

反射是什么

反射(Reflection) 是 Java 在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。

Class 类介绍:Java 虚拟机为每个类型管理一个 Class 对象,包含了与类有关的信息,当通过 javac 编译 Java 类文件时,生成的同名 .class 文件保存着该类的 Class 对象,JVM 加载一个类即是加载该 .class 文件。

JVM 在加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整的类的结构信息。可以通过反射看到这个类的结构

其中最主要的三个类 Field、Method 和 Constructor 分别用于描述类的域、方法和构造器,它们有一个共同的父类 AccessibleObject,它提供了访问控制检查的功能。

  • Field :描述类的域(属性),可以使用 get()set() 方法读取和修改 Field 对象关联的字段;
  • Method :描述类的方法,可以使用 invoke() 方法调用与 Method 对象关联的方法;
  • Constructor :描述类的构造器,可以用 Constructor 创建新的对象。

Java 反射机制提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

获取 Class 对象

public static void main(String[] args) throws ClassNotFoundException {
Class c1 = Class.forName("test.Temp.Student"); // 第1种,forName 方式获取Class对象
Class c2 = Student.class; // 第2种,直接通过类获取 Class 对象

Student student = new Student(19, "李四");
Class c3 = student.getClass(); // 第3种,通过调用对象的getClass()方法获取Class对象

if (c1 == c2 && c1 == c3) { // 可以通过 == 比较Class对象是否为同一个对象
System.out.println("c1、c2、c3 为同一个对象");
System.out.println(c1); // class reflect.Employee
}
}

获取继承关系

例如获取父类的 Class

public class Temp {

static class Person { }

static class Student extends Person { }

public static void main(String[] args) throws ClassNotFoundException {
Class s = Class.forName("test.Temp$Student");
Class superclass = s.getSuperclass();

System.out.println(superclass.getName());
}
}

由于一个类可能实现一个或多个接口,通过 Class 就可以查询到实现的接口类型。

public class Temp {

interface Person { }

interface School { }

static class Student implements Person, School { }

public static void main(String[] args) throws ClassNotFoundException {
Class s = Class.forName("test.Temp$Student");
Class[] interfaces = s.getInterfaces();

for (Class anInterface : interfaces) {
System.out.println(anInterface.getName());
}

}
}

注意:getInterfaces() 只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型

补充一个,单纯判断继承关系无需使用反射,直接使用 instanceof 关键字就行了

Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

通过反射来创建实例

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException,IllegalAccessException, InvocationTargetException, InstantiationException {

// 注意这里反射内部静态类的书写格式
Class c1 = Class.forName("test.Temp$Student");
Student student = (Student) c1.getConstructor(Integer.class, String.class).newInstance(18, "张三");
System.out.println(student);
}

获取类的全部信息

获取类名,构造器签名,所有的方法,所有的域(属性)和值,然后打印出来

// 获取这个Class的类名
String getName()

// 返回这个类的所有构造器的对象数组,包含保护和私有的构造器;
// 相近的方法 getConstructors() 则返回这个类的所有公有构造器的对象数组,不包含保护和私有的构造器
Constructor[] getDeclaredConstructors()


// 返回这个类或接口的所有方法,包括保护和私有的方法,不包括超类的方法;
// 相近的方法 getMethods() 则返回这个类及其超类的公有方法的对象数组,不含保护和私有的方法
Method[] getDeclaredMethods()


// 返回这个类的所有域的对象数组,包括保护域和私有域,不包括超类的域;
// 还有一个相近的API getFields(),返回这个类及其超类的公有域的对象数组,不含保护域和私有域
Field[] getDeclaredFields()


// 返回一个用于描述 Field、Method 和 Constructor 的修饰符的整形数值,
// 该数值代表的含义可通过 Modifier 这个类分析
int getModifiers()

Modifier 类 它提供了有关Field、Method和Constructor等的访问修饰符的信息,主要的方法有:

toString(int modifiers) // 返回整形数值 modifiers 代表的修饰符的字符串;
isAbstract() // 是否被abstract修饰;
isVolatile() // 是否被volatile修饰;
isPrivate() // 是否为private;
isProtected() // 是否为protected;
isPublic() // 是否为public;
isStatic() // 是否为static修饰;

运行时查看对象数据域的实际内容

运行时查看对象数据域实际内容的相关 API

// 返回数组类里组件类型的 Class,如果不是数组类则返回null
Class<?> getComponentType()

// 返回这个类是否为数组
// 同类型的 API 还有 isAnnotation、isAsciiDigit、isEnum、isInstance、isInterface、isLocalClass、isPrimitive 等
boolean isArray()

// 返回数组对象 obj 的长度
int Array.getLength(obj)

// 获取数组对象下标为 i 的元素
Object Array.get(obj, i)

// 返回这个类是否为8种基本类型之一,
// 即是否为 boolean, byte, char, short, int, long, float, 和 double 等原始类型
boolean isPrimitive()

// 获取指定名称的域对象
Field getField(String name)

// 当访问 Field、Method 和 Constructor 的时候 Java 会执行访问检查,
// 如果访问者没有权限将抛出 SecurityException,
// 譬如访问者是无法访问 private 修饰的域的。
// 通过设置 setAccessible(true) 可以取消 Java 的执行访问检查,
// 这样访问者就获得了指定 Field、Method 或 Constructor 访问权限
AccessibleObject.setAccessible(fields, true)

// 返回一个Class 对象,它标识了此 Field 对象所表示字段的声明类型
Class<?> Field.getType()

// 获取 obj 对象上当前域对象表示的属性的实际值,获取到的是一个Object对象,
// 实际使用中还需要转换成实际的类型,或者可以通过 getByte()、getChar、getInt() 等直接获取具体类型的值
Object Field.get(Object obj)

// 设置obj对象上当前域表示的属性的实际值
void Field.set(Object obj, Object value)

如果是私有字段,需要通过

AccessibleObject.setAccessible(fields, true)

将域设置为了可访问,取消了 Java 的执行访问检查,因此可以访问,如果不加会报异常 IllegalAccessException

调用任意方法

获取指定的 Method,参数 name 为要获取的方法名,parameterTypes 为指定方法的参数的 Class,由于可能存在多个同名的重载方法,所以只有提供正确的 parameterTypes 才能准确的获取到指定的 Method

Method getMethod(String name, Class<?>... parameterTypes) 

使用这个 invoke 执行方法,第一个参数执行该方法的对象,如果是 static 修饰的类方法,则传 null 即可;后面是传给该方法执行的具体的参数值

Object invoke(Object obj, Object... args) 

使用例子

public class Temp {

static class Student {
private void printHello() {
System.out.println("你好");
}

private static void printStaticHello() {
System.out.println("你好这是静态方法");
}
}

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 因为是 private 修饰的方法,所以需要使用 getDeclaredMethod 才能取得
Method printHello = Student.class.getDeclaredMethod("printHello");
Method printStaticHello = Student.class.getDeclaredMethod("printStaticHello");

// 实际上还是需要一个实例来执行这个方法(当然这个实例可以用反射创造出来)
printHello.invoke(new Student());
// 静态方法直接传 null 进去就行了
printStaticHello.invoke(null);
}
}

动态代理

在运行期动态创建一个 interface 实例的方法如下:

  1. 定义一个 InvocationHandler 实例,它负责实现接口的方法调用;
  2. 通过 Proxy.newProxyInstance() 创建 interface 实例,它需要3个参数:
  3. 使用的 ClassLoader,通常就是接口类的 ClassLoader;
  4. 需要实现的接口数组,至少需要传入一个接口进去;
  5. 用来处理接口方法调用的 InvocationHandler 实例。
  6. 将返回的 Object 强制转型为接口。

这个 InvocationHandler 用来处理如何执行接口里面的方法

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

具体使用例

public class Temp {

interface Person {
void sleep();
String speak();
String work(String task);
}


public static void main(String[] args) {
InvocationHandler handler = (Object proxy, Method method, Object[] mArgs) -> {
System.out.println("当前执行的任务是:" + method.getName());
switch (method.getName()) {
case "sleep":
System.out.println("现在要睡觉了!");
return null;
case "speak":
return "我现在在说话";
case "work":
return "我处理了任务:" + mArgs[0];
default:
return null;
}
};

Person person = (Person) Proxy.newProxyInstance(
Person.class.getClassLoader(),
new Class[] {Person.class}, // 传入要实现的接口(因为有些实例可能需要继承多个接口,所以这里传入的是一个数组)
handler // 传入处理调用方法的 InvocationHandler
);

// 调用这些方法
person.sleep();
System.out.println(person.speak());
System.out.println(person.work("写作业"));
}
}

实际上动态代理就是用反射对静态代理进行了一些 “升级”,如下代码就是转换成静态代理的样子(用了反射的)

// 对方法数量进行了简化
public class Temp {

interface Person {
void sleep() throws Throwable;
}

// 看这个代理类,其实本质和静态代理那套是一样的
static class PersonProxy implements Person {
InvocationHandler handler;

public PersonProxy(InvocationHandler handler) {
this.handler = handler;
}

@Override
public void sleep() throws Throwable {
handler.invoke(
this,
this.getClass().getMethod("sleep"),
null);
}
}


public static void main(String[] args) throws Throwable {
Person person = new PersonProxy((Object proxy, Method method, Object[] mArgs) -> {
System.out.println("代理执行了睡觉");
return null;
});

person.sleep();
}
}

反射的缺点

性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。

安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。

内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

Reference

参考资料 Java反射机制详解